from flask import Flask, jsonify, request, send_from_directory, Response, session, abort, redirect
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.sql import func
from sqlalchemy.dialects.mysql import JSON
from flask_admin import Admin, AdminIndexView
from flask_admin.contrib.sqla import ModelView
from flask_babel import Babel
from datetime import datetime, time
from werkzeug.middleware.proxy_fix import ProxyFix
from redis import Redis
from functools import wraps

import os
import secrets
import json
import random

from wechatpy.oauth import WeChatOAuth
from wechatpy.client import WeChatClient
from wechatpy.session.redisstorage import RedisStorage
from wechatpy.utils import check_signature
from wechatpy.exceptions import InvalidSignatureException
from wechatpy import parse_message
from wechatpy.replies import ArticlesReply

from prompts import explain_an0, explain_an1, explain_an2, gen_an2, gen_analysis

redis_client = Redis.from_url('redis://127.0.0.1:6379/0')
session_interface = RedisStorage(
    redis_client,
    prefix="wechatpy"
)
WECHAT_APP_ID = "wx4a0ac95c9ca187e4"
WECHAT_APP_SECRET = "bf6919713e252ddd1b8233a3bb7fa77a"
WECHAT_TOKEN = "lkdfj3epcr3qabe64jhh85ugurxsfjqv"
WECHAT_ENCODING_AESKEY = "WSHOTGRZxoFcbmsqOFcppezenZdYEGeEwPfnGFpW52U"
wechat_client = WeChatClient(
    WECHAT_APP_ID,
    WECHAT_APP_SECRET,
    session=session_interface
)

app = Flask(__name__, static_folder="static", static_url_path="/")
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://ceecs:Je2Hh2bNSaYSi6pC@127.0.0.1:3306/ceecs'
app.config['BABEL_DEFAULT_LOCALE'] = 'zh_CN'
app.secret_key = secrets.token_hex(16)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
db = SQLAlchemy(app)
babel = Babel(app)

subjects_order = ["政治", "历史", "地理", "物理", "化学", "生物", "技术"]


def subjects_to_int(subjects_list):
    binary_representation = ''.join(
        ['1' if subject in subjects_list else '0' for subject in subjects_order])
    return int(binary_representation, 2)


def int_to_subjects(subjects_int):
    binary_representation = format(subjects_int, '07b')
    return [subject for subject, bit in zip(subjects_order, binary_representation) if bit == '1']


def gen_woa_qrcode_scene_id():
    now = datetime.now()
    midnight = datetime.combine(now.date(), time(0, 0))
    delta = now - midnight
    return int(f"{delta.seconds}{random.randint(100, 999)}")


class User(db.Model):
    platform = db.Column(db.String(80), primary_key=True)
    unique_id = db.Column(db.String(255), primary_key=True)
    attributes = db.Column(JSON)

    def to_dict(self):
        base_dict = {c.name: getattr(self, c.name)
                     for c in self.__table__.columns}
        if base_dict.get('attributes'):
            base_dict = {**base_dict, **base_dict['attributes']}
            del base_dict['attributes']  # 如果你不希望保留原始的'attributes'字段，可以删除
            if 'logs' in base_dict:
                del base_dict['logs']
        return base_dict


class Q(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    question = db.Column(db.Text)
    detail = db.Column(db.Text)


class QA(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    select_items = db.Column(db.Text)
    placeholder = db.Column(db.Text)
    question = db.Column(db.Text)
    canCustom = db.Column(db.Boolean)
    type = db.Column(db.Integer)


class Req(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    province = db.Column(db.String(50), nullable=False)
    university = db.Column(db.String(255), nullable=False)
    category = db.Column(db.String(255), nullable=False)
    majors = db.Column(db.Text, nullable=True)
    level = db.Column(db.String(50), nullable=False)
    subjects = db.Column(db.Integer, nullable=False)


class QUserView(ModelView):
    def __init__(self, dbsession):
        super().__init__(User, dbsession, name='用户')

    column_labels = {
        'id': 'ID',
        'unique_id': '唯一键',
        'attributes': '属性'
    }


class QModelView(ModelView):
    def __init__(self, dbsession):
        super().__init__(Q, dbsession, name='问题')

    column_labels = {
        'platform': '平台',
        'question': '问题',
        'detail': '描述'
    }


class QAModelView(ModelView):
    def __init__(self, dbsession):
        super().__init__(QA, dbsession, name='问答')
    column_labels = {
        'id': 'ID',
        'select_items': '选项',
        'placeholder': '占位符',
        'question': '问题',
        'canCustom': '是否自定义',
        'type': '类型'
    }


class ReqModelView(ModelView):
    def __init__(self, dbsession):
        super().__init__(Req, dbsession, name='选科需求')
    column_labels = {
        'id': 'ID',
        'province': '省份',
        'university': '院校名称',
        'category': '专业（类）名称',
        'majors': '类中所含专业',
        'level': '层次',
        'subjects': '选考科目要求'
    }


class MyAdminIndexView(AdminIndexView):

    def is_accessible(self):
        me = session.get('me')
        return me and me['admin'] == 1


admin = Admin(app, index_view=MyAdminIndexView(),
              name='品智AI选科助手后台', template_mode='bootstrap3')

admin.add_view(QUserView(db.session))
admin.add_view(QModelView(db.session))
admin.add_view(QAModelView(db.session))
admin.add_view(ReqModelView(db.session))


def get_answer_from_qas(data, target_type):
    item = next((item for item in data if item['type'] == target_type), None)
    return item["answer"] if item and "answer" in item and item["answer"] else ""


def getWeChatOAuth(redirect_url):
    return WeChatOAuth(WECHAT_APP_ID, WECHAT_APP_SECRET, redirect_url, 'snsapi_userinfo')


def record_log(api_method_name):
    def decorator(f):
        @wraps(f)
        def wrapped_function(*args, **kwargs):
            input_data = request.get_json()

            # 调用实际的路由函数
            response = f(*args, **kwargs)
            response_data = response.get_json()

            # 记录日志
            me = session.get('me')
            platform = me['platform']
            unique_id = me['unique_id']
            if platform and unique_id:
                user = User.query.filter_by(
                    platform=platform, unique_id=unique_id).first()
                if user:
                    log_entry = {
                        "timestamp": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                        "input": input_data,
                        "output": response_data,
                        "ip": request.remote_addr
                    }

                    if "logs" not in user.attributes:
                        user.attributes["logs"] = {}
                    if api_method_name not in user.attributes["logs"]:
                        user.attributes["logs"][api_method_name] = []

                    # 在列表的开头插入新日志
                    user.attributes["logs"][api_method_name].insert(
                        0, log_entry)
                    # 如果日志列表超过5条，删除最旧的条目
                    while len(user.attributes["logs"][api_method_name]) > 5:
                        user.attributes["logs"][api_method_name].pop()
                        
                    flag_modified(user, "attributes")
                    db.session.commit()

            return response
        return wrapped_function
    return decorator


@app.before_request
def handle_oauth():
    code = request.args.get('code', None)
    if code:
        wechat_oauth = getWeChatOAuth(request.url)
        platform = "wechat-official-account"
        try:
            wechat_oauth.fetch_access_token(code)
            me = wechat_oauth.get_user_info()
        except Exception as e:
            abort(403, description=f'{e.errcode}: {e.errmsg}')
        else:
            openid = me["openid"]
            if openid:
                user = User.query.filter_by(
                    platform=platform, unique_id=openid).first()
                if not user:
                    login_attributes = {
                        "last_login": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                        "login_count": 1,
                        "login_method": platform,
                        "last_login_ip": request.remote_addr
                    }
                    user = User(platform=platform, unique_id=openid,
                                attributes={**me, **login_attributes})
                    db.session.add(user)
                else:
                    current_count = user.attributes.get("login_count", 0)
                    user.attributes["last_login"] = datetime.now().strftime(
                        '%Y-%m-%d %H:%M:%S')
                    user.attributes["login_count"] = current_count + 1
                    user.attributes["last_login_ip"] = request.remote_addr
                    flag_modified(user, "attributes")
                db.session.commit()
                session['me'] = user.to_dict()
                return
            abort(403, description=f'非法授权code，请重试，code: {code}')
    if request.path.startswith("/admin"):
        me = session.get('me')
        if not me or me['admin'] != 1:
            return redirect(request.root_url)
    if request.path in ["/api/qs"]:
        return
    if not request.path.startswith("/api"):
        return
    if session.get('me', None):
        return
    abort(403, description=f'未授权，请登录')


@app.route('/woa_qrcode')
def woa_qrcode():
    user_agent = request.headers.get('User-Agent', '')
    if 'MicroMessenger' in user_agent:
        wechat_oauth = getWeChatOAuth(request.root_url)
        return jsonify({'status': 'success', 'data': wechat_oauth.authorize_url, 'isinwechat': True})
    else:
        scene_id = gen_woa_qrcode_scene_id()
        res = wechat_client.qrcode.create({
            'expire_seconds': 1800,
            'action_name': 'QR_SCENE',
            'action_info': {
                'scene': {'scene_id': scene_id},
            }
        })
        qrcode_url = wechat_client.qrcode.get_url(res)
        redis_client.setex(
            f'woa_qrcode_scene_id_{scene_id}', 1800, json.dumps(None))
        return jsonify({'status': 'success', 'data': qrcode_url, 'code': scene_id})


@app.route('/me')
def me():
    me = None
    code = request.args.get('woa_qrcode_scene_id', None)
    if "me" in session:
        me = session['me']
    elif code:
        me_redis = redis_client.get(f'woa_qrcode_scene_id_{code}')
        if me_redis:
            me = json.loads(me_redis)
            if me is not None and 'unique_id' in me:
                session['me'] = me
                redis_client.delete(f'woa_qrcode_scene_id_{code}')
    return jsonify({'status': 'success', 'data': me})


@app.route('/wx', methods=['POST', 'GET'])
def wx():
    if request.method == 'GET':
        signature = request.args.get('signature')
        timestamp = request.args.get('timestamp')
        nonce = request.args.get('nonce')
        echostr = request.args.get('echostr')
        try:
            check_signature(WECHAT_TOKEN, signature, timestamp, nonce)
            return Response(echostr)
        except InvalidSignatureException:
            return 'error'
    else:
        msg = parse_message(request.data)
        from wechatpy.events import ScanEvent, SubscribeScanEvent
        if isinstance(msg, ScanEvent) or isinstance(msg, SubscribeScanEvent):
            openid = msg.source
            platform = "wechat-official-account"
            user = User.query.filter_by(
                platform=platform, unique_id=openid).first()
            if not user:
                login_attributes = {
                    "last_login": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                    "login_count": 1,
                    "login_method": platform,
                    "last_login_ip": request.remote_addr
                }
                user = User(platform=platform, unique_id=openid,
                            attributes={**login_attributes})
                db.session.add(user)
            else:
                current_count = user.attributes.get("login_count", 0)
                user.attributes["last_login"] = datetime.now().strftime(
                    '%Y-%m-%d %H:%M:%S')
                user.attributes["login_count"] = current_count + 1
                user.attributes["last_login_ip"] = request.remote_addr
                flag_modified(user, "attributes")
            db.session.commit()
            redis_client.setex(
                f'woa_qrcode_scene_id_{msg.scene_id}', 1800, json.dumps(user.to_dict()))
            reply = ArticlesReply(message=msg)
            reply.add_article({
                'title': '欢迎关注我们',
                'description': '品智AI选科助手欢迎您',
                'image': 'https://pinzhi.online/logo72.png',
                'url': 'https://pinzhi.online'
            })
            return Response(reply.render())


@app.route('/api/explain', methods=['POST'])
@record_log('explain')
def explain():
    data = request.get_json()
    _type = data['type']
    explain = [explain_an0, explain_an1, explain_an2][_type](data['an'])
    return jsonify({'status': 'success', 'explain': explain['explain'], 'advise': explain['advise']})


@app.route('/api/analysis', methods=['POST'])
@record_log('analysis')
def analysis():
    data = request.get_json()
    qas = data["qas"]
    q = data["q"]
    an0, an1, an2 = [get_answer_from_qas(qas, t) for t in range(3)]
    an2_after_gpt = gen_an2(an2)
    analysis_json = gen_analysis(
        an0, an1, an2_after_gpt["result"] if "result" in an2_after_gpt else "", q)
    return jsonify({'status': 'success', 'data': analysis_json, 'an2_after_analysis': an2_after_gpt})


@app.route('/api/req/<subjects>')
def req(subjects):
    subjects_int = subjects_to_int([subjects[i:i+2] for i in range(0, 6, 2)])
    equal_records = Req.query.filter_by(subjects=subjects_int).order_by(
        Req.majors.desc()).limit(10).all()
    if len(equal_records) < 10:
        subset_limit = 10 - len(equal_records)
        subset_records = Req.query.filter(
            (Req.subjects.op('&')(subjects_int)) == Req.subjects,
            Req.subjects != 0
        ).order_by(Req.majors.desc()).limit(subset_limit).all()
    else:
        subset_records = []
    if len(equal_records) + len(subset_records) < 10:
        empty_set_limit = 10 - len(equal_records) - len(subset_records)
        empty_set_records = Req.query.filter_by(subjects=0).order_by(
            Req.majors.desc()).limit(empty_set_limit).all()
    else:
        empty_set_records = []

    # 合并结果
    final_results = equal_records + subset_records + empty_set_records
    return jsonify({'status': 'success', 'data': [{'id': req.id, 'province': req.province, 'university': req.university, 'category': req.category, 'majors': req.majors, 'level': req.level} for req in final_results]})


@app.route('/api/qs')
def qs():
    questions = Q.query.order_by(func.random()).limit(5).all()
    return jsonify([{'id': q.id, 'question': q.question, 'detail': q.detail} for q in questions])


@app.route('/api/qas')
def qas():
    qas_list = []
    types = list(range(3))
    random.shuffle(types)
    for type in types:  # assuming types are 0, 1, 2
        qa = QA.query.filter_by(type=type).order_by(func.random()).first()
        if qa:  # check if a question of this type exists
            try:
                selectItems = json.loads(qa.select_items)
            except:
                selectItems = None
            qas_list.append({'id': qa.id, 'selectItems': selectItems, 'placeholder': qa.placeholder,
                            'question': qa.question, 'canCustom': qa.canCustom, 'type': qa.type})
    return jsonify(qas_list)


@app.route('/', defaults={'path': ''})
@app.route('/<path>')
def serve(path):
    if path != "" and os.path.exists(app.static_folder + '/' + path):
        return send_from_directory(app.static_folder, path)
    else:
        return send_from_directory(app.static_folder, 'index.html')


if __name__ == "__main__":
    with app.app_context():
        db.create_all()
    app.run(host='0.0.0.0', debug=True)
